5.15. Метатаблицы и метаметоды
Метатаблицы и метаметоды
Пример таблицы
Unit = {}
function Unit:new()
local instance = {
name = "Имя",
intel = 10,
agility = 10,
strength = 10,
health = 100,
mana = 50,
level = 1
}
setmetatable(instance, self)
self.__index = self
return instance
end
function Unit:damage()
return (self.intel + self.agility + self.strength) + (self.level * 2)
end
function Unit:attack(target)
local dmg = self:damage()
print(self.name .. " атакует " .. target.name .. " и наносит " .. dmg .. " единиц урона.")
target.health = target.health - dmg
print(target.name .. " теперь имеет " .. target.health .. " здоровья.")
end
local warrior = Unit:new()
warrior.name = "Воин"
warrior.intel = 5
warrior.agility = 15
warrior.strength = 30
local mage = Unit:new()
mage.name = "Маг"
mage.intel = 35
mage.agility = 10
mage.strength = 5
warrior:attack(mage)
mage:attack(warrior)
Таблица Unit служит шаблоном для создания объектов. В Lua таблицы представляют универсальную структуру данных, способную хранить пары ключ-значение и реализовывать объектное поведение через метатаблицы.
Метод new создаёт новую таблицу instance с начальными значениями полей. Функция setmetatable устанавливает метатаблицу для экземпляра, связывая его с таблицей Unit. Поле __index в метатаблице обеспечивает делегирование: при обращении к отсутствующему ключу в экземпляре поиск продолжается в таблице класса. Конструктор возвращает готовый объект с установленной метатаблицей.
Синтаксис function Unit:damage() эквивалентен function Unit.damage(self). Двоеточие автоматически передаёт первый параметр self, ссылающийся на объект, у которого вызывается метод. Такой подход упрощает вызов методов и делает код более читаемым.
Метод damage обращается к полям объекта через self.поле. Выражение складывает характеристики персонажа и добавляет бонус уровня. Каждый вызов метода производит актуальный расчёт без сохранения промежуточного значения.
Метод attack принимает целевой объект в параметре target. Локальная переменная dmg сохраняет результат вызова self:damage() для повторного использования. Оператор конкатенации .. объединяет строки и числовые значения в единое сообщение. Оператор присваивания с вычитанием уменьшает здоровье цели. Функция print выводит сообщение в консоль с автоматическим переносом строки.
Вызов Unit:new() создаёт новый объект с начальными значениями. Ключевое слово local ограничивает область видимости переменной текущим блоком кода. После создания объекта значения его полей изменяются через прямое присваивание. Каждый объект хранит собственный набор значений независимо от других экземпляров.
Синтаксис warrior:attack(mage) передаёт объект warrior как параметр self в метод attack, а объект mage как параметр target. Такой вызов обеспечивает доступ к полям и методам обоих объектов внутри тела метода. Последовательные вызовы демонстрируют изменение состояния объектов при взаимодействии.
Метатаблица с полем __index реализует прототипное наследование. При обращении к несуществующему полю в экземпляре интерпретатор автоматически проверяет таблицу, указанную в __index. Такой механизм позволяет всем экземплярам разделять одни и те же методы, хранящиеся в таблице класса, без дублирования кода.
Lua использует динамическую типизацию. Переменные не имеют фиксированного типа, тип значения определяется во время выполнения. Числовые значения автоматически преобразуются в строки при конкатенации. Отсутствие строгой типизации упрощает написание кода, но требует внимательности при операциях с разнотипными данными.
Интерпретатор Lua последовательно выполняет инструкции файла. Создаются два объекта с различными характеристиками. Первый вызов метода уменьшает здоровье мага на величину урона воина. Второй вызов метода уменьшает здоровье воина с учётом его текущих характеристик. Каждое действие сопровождается выводом информационного сообщения, отражающего изменение состояния объектов.
Таблицы - местный ООП
Таблицы используются повсеместно: как массивы, словари, объекты, модули и даже классы. Однако их истинная гибкость проявляется не только в структуре хранения данных, но и в возможности динамического переопределения поведения операций над ними. Именно эту возможность обеспечивают метатаблицы и метаметоды.
Таблица в Lua — это ассоциативный массив, реализованный как хеш-таблица (или комбинация массива и хеша для оптимизации). Она представляет собой коллекцию пар ключ–значение, где ключи и значения могут быть любыми типами, кроме nil. В памяти таблица организована как динамическая структура, поддерживающая эффективные операции вставки, поиска и удаления.
local t = { x = 10, y = 20 }
Это эквивалентно:
local t = {}
t.x = 10
t.y = 20
Внутри интерпретатора каждая таблица содержит:
- Указатель на массив компонентов (для числовых индексов, начиная с 1),
- Хеш-таблицу для произвольных ключей,
- Ссылку на метатаблицу (если она установлена).
Таблицы — единственный составной тип в Lua, способный к изменению поведения через внешние механизмы. Это достигается за счёт метатаблиц.
Метатаблица
Метатаблица (metatable) — это обычная таблица, присоединённая к другой таблице (или userdata), которая определяет специальное поведение последней при выполнении определённых операций.
Каждая таблица может иметь не более одной метатаблицы. Метатаблица не наследуется автоматически; она устанавливается явно с помощью функции setmetatable или возвращается функцией getmetatable.
local t = {}
local mt = {}
setmetatable(t, mt)
assert(getmetatable(t) == mt)
Метатаблица действует как описатель поведения: она не хранит данные объекта напрямую, но задаёт правила, по которым объект реагирует на операции, такие как сложение, доступ к полям, вызов как функции и т.д.
Метаметод
Метаметод (metamethod) — это поле внутри метатаблицы, имя которого начинается с двойного подчёркивания (__), и которое определяет поведение при определённой операции.
Например, метаметод __add определяет, что происходит при использовании оператора + с таблицей.
Каждый метаметод соответствует конкретному событию в жизненном цикле операции. Когда интерпретатор сталкивается с операцией над таблицей, он проверяет наличие соответствующего метаметода в её метатаблице и, если он найден, вызывает его вместо стандартного поведения.
При попытке выполнить операцию a + b, если один из операндов — таблица с метатаблицей, содержащей __add, вызывается именно эта функция. Если нет — генерируется ошибка (если оба операнда не являются числами).
Наиболее важные метаметоды
Рассмотрим наиболее важные метаметоды и их применение.
__index— перехват чтения отсутствующих полей. Определяет поведение при попытке доступа к несуществующему ключу.- Если
__index— функция, она вызывается с двумя аргументами: таблицей и ключом. - Если
__index— таблица, поиск продолжается в этой таблице.
- Если
local defaults = { color = "white", size = "medium" }
local obj = {}
setmetatable(obj, { __index = defaults })
print(obj.color) -- "white" (берётся из defaults)
Этот механизм лежит в основе делегирования и используется для эмуляции наследования.
2. __newindex — перехват записи в поля. Вызывается при попытке присвоить значение новому (или существующему) ключу.
- Позволяет контролировать, как и куда записываются данные.
- Может использоваться для создания защищённых таблиц, прокси или реактивных систем.
local t = {}
local proxy = {}
setmetatable(proxy, {
__newindex = function(tbl, key, value)
print("Запись:", key, "=", value)
rawset(t, key, value) -- обход метаметода
end
})
proxy.x = 10 -- Вывод: Запись: x = 10
Важно: Для обхода метаметодов используются rawget, rawset, rawlen.
__add,__sub,__mul,__div,__mod,__pow— арифметические метаметоды. Позволяют перегружать арифметические операторы.
local Vec2 = { x = 0, y = 0 }
function Vec2:new(x, y)
local obj = { x = x or 0, y = y or 0 }
setmetatable(obj, self)
self.__index = self
return obj
end
function Vec2:__add(other)
return Vec2:new(self.x + other.x, self.y + other.y)
end
local a = Vec2:new(1, 2)
local b = Vec2:new(3, 4)
local c = a + b
print(c.x, c.y) -- 4 6
Таким образом, Lua поддерживает операторную перегрузку, аналогично C++ или Python.
__call— вызов таблицы как функции. Позволяет использовать таблицу как вызываемый объект.
local Counter = { count = 0 }
function Counter:__call()
self.count = self.count + 1
return self.count
end
setmetatable(Counter, Counter)
print(Counter()) -- 1
print(Counter()) -- 2
Часто применяется для создания фабрик, синглтонов или DSL.
__tostring— строковое представление. Определяет, как таблица конвертируется в строку (например, при print).
function Vec2:__tostring()
return string.format("Vec2(%g, %g)", self.x, self.y)
end
print(a) -- Vec2(1, 2)
Без __tostring print(t) выводит что-то вроде table: 0x....
__eq,__lt,__le— сравнение таблиц. По умолчанию таблицы сравниваются по ссылке. Эти метаметоды позволяют задать логическое равенство или порядок.
function Vec2:__eq(other)
return self.x == other.x and self.y == other.y
end
setmetatable(Vec2, { __eq = Vec2.__eq })
local v1 = Vec2:new(1, 2)
local v2 = Vec2:new(1, 2)
print(v1 == v2) -- true (если метаметод установлен корректно)
Ограничение: __eq работает только при явном сравнении двух таблиц с одним и тем же метаметодом.
Классы-таблицы
Lua не имеет встроенных классов, но предоставляет все средства для эмуляции объектно-ориентированного программирования на основе таблиц и делегирования.
«Класс» в Lua — это таблица, выступающая как прототип и хранилище методов.
Person = {}
Person.__index = Person
function Person:new(name, age)
local instance = setmetatable({}, self)
instance.name = name
instance.age = age
return instance
end
function Person:greet()
print("Привет, меня зовут " .. self.name)
end
Наследование реализуется путём установки родительского прототипа как метатаблицы дочернего класса.
Student = Person:new() -- Student наследует от Person
Student.__index = Student
function Student:new(name, age, grade)
local instance = Person:new(name, age)
setmetatable(instance, self)
instance.grade = grade
return instance
end
local s = Student:new("Анна", 20, "A")
s:greet() -- Привет, меня зовут Анна
Цепочка поиска методов:
- Поле ищется в самой таблице.
- Если не найдено — в
__indexметатаблицы. - Рекурсивно, пока не будет найдено или не завершится цепочка.
Это прототипное наследование, аналогичное JavaScript.
Хотя Lua не поддерживает полиморфизм в классическом смысле (перегрузка функций по сигнатурам), метаметоды обеспечивают поведенческий полиморфизм:
- Один и тот же оператор (+, #, () и т.д.) вызывает разные функции в зависимости от типа операндов.
- Это ад-хок полиморфизм, близкий к тому, что реализован в Python (
__add__) или Ruby (+).
-- Сложение векторов
mt_vec.__add = add_vectors
-- Сложение матриц
mt_mat.__add = add_matrices
-- Оператор + работает полиморфно
a + b -- вызовет нужную реализацию в зависимости от типов
Lua предлагает минималистичный, но мощный механизм, позволяющий строить сложные абстракции поверх простых примитивов. Однако такая гибкость требует дисциплины: без соглашений код может стать трудным для понимания. Метатаблицы и метаметоды — это фундаментальная абстракция, превращающая Lua из простого скриптового языка в мощную платформу для создания доменно-ориентированных языков (DSL), игровых движков, конфигурационных систем и сложных ООП-архитектур.